﻿#region Copyright(C) 2018 FI Ltd. All rights reserved.
// ==================================================================================================
// Copyright(C) 2018 FI Ltd. All rights reserved.
//
// SYSTEM NAME      : FI.Net Application Framework
// COMPONENT ID     : ApplicationContext
// COMPONENT DESC   : Session based contextual data sharing.
//
// CREATED DATE/BY  : 18 Aug 2018 / Tian Mi
//
// REVISION HISTORY :
// DATE/BY            SR#/CS/PM#/OTHERS      DESCRIPTION OF CHANGE
// ==================================================================================================
#endregion

using FI.Core.ApplicationContexts.Configuration;
using FI.Core.IoC;
using FI.Core.Properties;
using FI.Core.Utilities;
using System;
using System.Globalization;
using System.Threading;
using System.Web;

namespace FI.Core.ApplicationContexts
{
    /// <summary>
    /// supply the basic application context opertions
    /// </summary>
    public sealed class ApplicationContext
    {
        #region Constants
        /// <summary>
        /// define the context header's namespace.
        /// </summary>
        public const string ContextHeaderNamespace = Constants.Namespace;

        /// <summary>
        /// define the context header's local name.
        /// </summary>
        public const string ContextHeaderLocalName = "Applicationcontext";
        /// <summary>
        /// The context http header name.
        /// </summary>
        public const string ContextHttpHeaderName = "FI.Core.Applicationcontext";

        private const string KeyofUserId        = "FI.ApplicationContexts.UserId";
        private const string KeyofUserName      = "FI.ApplicationContexts.UserName";
        private const string KeyofTransactionId = "FI.ApplicationContexts.TransactionId";
        private const string KeyofTimeZone      = "FI.ApplicationContexts.TimeZone";
        private const string KeyofCulture       = "FI.ApplicationContexts.Culture";
        private const string KeyofUICulture     = "FI.ApplicationContexts.UICulture";
        private const string KeyofSessionId     = "FI.ApplicationContexts.SessionId";
        private static object syncHelper = new object();

        #endregion

        #region Current
        private static ApplicationContext current = null;

        /// <summary>
        /// get the current application context
        /// </summary>
        public static ApplicationContext Current
        {
            get
            {
                if (null == current)
                {
                    lock (syncHelper)
                    {
                        if (null == current)
                        {
                            current = CreateApplicationContext();
                        }
                    }
                }

                return current;
            }
        }

        private static ApplicationContext CreateApplicationContext()
        {
            ApplicationContextSettings appContextSettings;
            if (!ConfigManager.TryGetConfigurationSection<ApplicationContextSettings>(out appContextSettings))
            {
                return new ApplicationContext(new CallContextLocator(), ContextAttachBehavior.Clear);     
            }
            ContextLocatorBase contextLocator = ServiceLocatorFactory.GetServiceLocator().GetService<ContextLocatorBase>(appContextSettings.DefaultContextLocator);
            ContextAttachBehavior contextAttachBehavior = appContextSettings.ContextAttachBehavior;
            return new ApplicationContext(contextLocator, contextAttachBehavior);            
        }

        private ApplicationContext(ContextLocatorBase contextLocator, ContextAttachBehavior contextAttachBehavior)
        {
            Guard.ArgumentNotNull(contextLocator, "contextLocator");
            this.ContextLocator = contextLocator;
            this.ContextAttachBehavior = contextAttachBehavior;
        }
        #endregion

        #region Properties
        /// <summary>
        /// get or private set the context locator interface.
        /// </summary>
        public ContextLocatorBase ContextLocator { get; private set; }

        /// <summary>
        /// get or private set the context attach behavior.
        /// </summary>
        public ContextAttachBehavior ContextAttachBehavior { get; private set; }

        #endregion

        #region Indexes

        /// <summary>
        /// Gets the <see cref="FI.Core.ApplicationContexts.ContextItem"/> with the specified key.
        /// </summary>
        /// <value>The <see cref="FI.Core.ApplicationContexts.ContextItem"/>.</value>
        public ContextItem this[string key]
        {
            get { return ContextLocator.GetContextItem(key); }
        }
        #endregion

        #region GetValue<TValue> & GetValue<TValue>
        /// <summary>
        /// Gets the value of the <see cref="ContextItem"/> with the specified key.
        /// </summary>
        /// <typeparam name="TValue">The type of the value of <see cref="ContextItem"/></typeparam>
        /// <param name="key">The key of the <see cref="ContextItem"/> to get.</param>
        /// <returns>The value of the <see cref="ContextItem"/> to get. </returns>
        public TValue GetValue<TValue>(string key)
        {
            Guard.ArgumentNotNull(key, "key");
            ContextItem contextItem = ContextLocator.GetContextItem(key);
            return null == contextItem
                       ? default(TValue)
                       : (TValue)contextItem.Value;
        }

        /// <summary>
        /// Sets the context.
        /// </summary>
        /// <param name="key">The key of the <see cref="ContextItem" />.</param>
        /// <param name="value">The value of the <see cref="ContextItem" />.</param>
        /// <exception cref="System.InvalidOperationException"></exception>
        public void SetContext(string key, object value)
        {
            Guard.ArgumentNotNull(key, "key");
            ContextItem contextItem = this.ContextLocator.GetContextItem(key);

            if (null != contextItem && contextItem.ReadOnly)
            {
                throw new InvalidOperationException(ResourceUtility.Format(Resources.ExceptionCannotModifyReadonlyValue));
            }

            if (null != contextItem)
            {
                contextItem.Value = value;
            }
            else
            {
                contextItem = new ContextItem(key, value, false);
                ContextLocator.SetContextItem(contextItem);
            }
        }


        /// <summary>
        /// Sets the context.
        /// </summary>
        /// <param name="key">The key of the <see cref="ContextItem" />.</param>
        /// <param name="value">The value of the <see cref="ContextItem" />.</param>
        /// <param name="isLocal">if set to <c>true</c> [is local].</param>
        /// <exception cref="System.InvalidOperationException"></exception>
        public void SetContext(string key, object value, bool isLocal)
        {
            Guard.ArgumentNotNull(key, "key");
            ContextItem contextItem = this.ContextLocator.GetContextItem(key);

            if (null != contextItem && contextItem.ReadOnly)
            {
                throw new InvalidOperationException(ResourceUtility.Format(Resources.ExceptionCannotModifyReadonlyValue));
            }

            if (null != contextItem && contextItem.IsLocal == isLocal)
            {
                contextItem.Value = value;
            }
            else
            {
                contextItem = new ContextItem(key, value, isLocal);
                ContextLocator.SetContextItem(contextItem);
            }
        }
        #endregion

        #region Common Contexts
        /// <summary>
        /// Get or set the Id of the current user.
        /// </summary>
        /// <value>The the Id of the current user.</value>
        public string UserId
        {
            get
            {
                var userIdString = GetValue<string>(KeyofUserId);
                if (string.IsNullOrEmpty(userIdString))
                {
                    SetContext(KeyofUserId, string.Empty);
                    return string.Empty;
                }
                return GetValue<string>(KeyofUserId);
            }
            set
            {
                SetContext(KeyofUserId, value);
            }
        }

        /// <summary>
        /// Gets the user id context item.
        /// </summary>
        /// <value>The user id context item.</value>
        public ContextItem UserIdContextItem
        {
            get
            {
                var userIdString = GetValue<string>(KeyofUserId);
                if (string.IsNullOrEmpty(userIdString))
                {
                    SetContext(KeyofUserId, string.Empty);
                }
                return this[KeyofUserId];
            }
        }

        /// <summary>
        /// Get or set the Id of the current session.
        /// </summary>
        /// <value>The the Id of the current session.</value>
        public string SessionId
        {
            get
            {
                string sessionIdString = GetValue<string>(KeyofSessionId);
                if (string.IsNullOrEmpty(sessionIdString))
                {
                    SetContext(KeyofSessionId, string.Empty);
                    return string.Empty;
                }
                return GetValue<string>(KeyofSessionId);
            }
            set
            {
                SetContext(KeyofSessionId, value);
            }
        }

        /// <summary>
        /// Gets the session id context item.
        /// </summary>
        /// <value>The session id context item.</value>
        public ContextItem SessionIdContextItem
        {
            get
            {
                string sessionIdString = GetValue<string>(KeyofSessionId);
                if (string.IsNullOrEmpty(sessionIdString))
                {
                    SetContext(KeyofSessionId, string.Empty);
                }
                return this[KeyofSessionId];
            }
        }

        /// <summary>
        /// Get or set the name of the current user.
        /// </summary>
        /// <value>The name of the current user.</value>
        public string UserName
        {
            get
            {
                string userNameString = GetValue<string>(KeyofUserName);
                if (string.IsNullOrEmpty(userNameString))
                {
                    string userName = string.Empty;
                    if (null != HttpContext.Current && null != HttpContext.Current.User)
                    {
                        userName = HttpContext.Current.User.Identity.Name;
                    }
                    if (string.IsNullOrEmpty(userName) && null != Thread.CurrentPrincipal)
                    {
                        userName = Thread.CurrentPrincipal.Identity.Name;
                    }
                    SetContext(KeyofUserName, userName);
                }
                return GetValue<string>(KeyofUserName);
            }
            set
            {
                SetContext(KeyofUserName, value);
            }
        }

        /// <summary>
        /// Gets the user name context item.
        /// </summary>
        /// <value>The user name context item.</value>
        public ContextItem UserNameContextItem
        {
            get
            {
                string userNameString = GetValue<string>(KeyofUserName);
                if (string.IsNullOrEmpty(userNameString))
                {
                    SetContext(KeyofUserName, string.Empty);
                }
                return this[KeyofUserName];
            }
        }
        /// <summary>
        /// Gets or sets the id of the current ambient transaction.
        /// </summary>
        /// <value>The transaction id.</value>
        public string TransactionId
        {
            get
            {
                var transactionIdString = GetValue<string>(KeyofTransactionId);
                if (string.IsNullOrEmpty(transactionIdString))
                {
                    SetContext(KeyofTransactionId, string.Empty);
                    return string.Empty;
                }
                return GetValue<string>(KeyofTransactionId);
            }
            set
            {
                SetContext(KeyofTransactionId, value);
            }
        }

        /// <summary>
        /// Gets the transaction id context item.
        /// </summary>
        /// <value>The transaction id context item.</value>
        public ContextItem TransactionIdContextItem
        {
            get
            {
                var transactionIdString = GetValue<string>(KeyofTransactionId);
                if (string.IsNullOrEmpty(transactionIdString))
                {
                    SetContext(KeyofTransactionId, string.Empty);
                }
                return this[KeyofTransactionId];
            }
        }
        /// <summary>
        /// Gets or sets the time zone.
        /// </summary>
        /// <value>The time zone.</value>
        public TimeZoneInfo TimeZone
        {
            get
            {
                var timeZoneString = GetValue<string>(KeyofTimeZone);
                if (string.IsNullOrEmpty(timeZoneString))
                {
                    SetContext(KeyofTimeZone, TimeZoneInfo.Local.ToSerializedString());
                    return TimeZoneInfo.Local;
                }
                return TimeZoneInfo.FromSerializedString(timeZoneString);
            }
            set
            {
                Guard.ArgumentNotNull(value, "value");
                SetContext(KeyofTimeZone, value.ToSerializedString());
            }
        }


        /// <summary>
        /// Gets the time zone context item.
        /// </summary>
        /// <value>The time zone context item.</value>
        public ContextItem TimeZoneContextItem
        {
            get
            {
                if (string.IsNullOrEmpty(KeyofTimeZone))
                {
                    SetContext(KeyofTimeZone, TimeZoneInfo.Local);
                }
                return this[KeyofTimeZone];
            }
        }

        /// <summary>
        /// Gets or sets the current culture.
        /// </summary>
        /// <value>The current culture.</value>
        public CultureInfo Culture
        {
            get
            {
                var cultureString = GetValue<string>(KeyofCulture);
                if (string.IsNullOrEmpty(cultureString))
                {
                    SetContext(KeyofCulture, CultureInfo.CurrentCulture.Name);
                    return CultureInfo.CurrentCulture;
                }
                return new CultureInfo( GetValue<string>(KeyofCulture));
            }
            set
            {
                Guard.ArgumentNotNull(value, "value");
                SetContext(KeyofCulture, value.Name);
            }
        }

        /// <summary>
        /// Gets the culture context item.
        /// </summary>
        /// <value>The culture context item.</value>
        public ContextItem CultureContextItem
        {
            get
            {
                if (string.IsNullOrEmpty(KeyofCulture))
                {
                    SetContext(KeyofUICulture, CultureInfo.CurrentCulture.Name);
                }
                return this[KeyofCulture];
            }
        }

        /// <summary>
        /// Gets or sets the current UI culture.
        /// </summary>
        /// <value>The current UI culture.</value>
        public CultureInfo UICulture
        {
            get
            {
                var UICultureString = GetValue<string>(KeyofUICulture);
                if (string.IsNullOrEmpty(UICultureString))
                {
                    SetContext(KeyofUICulture, CultureInfo.CurrentUICulture.Name);
                    return CultureInfo.CurrentUICulture;
                }
                return new CultureInfo( GetValue<string>(KeyofUICulture));
            }
            set
            {
                Guard.ArgumentNotNull(value, "value");
                SetContext(KeyofUICulture, value.Name);
            }
        }

        /// <summary>
        /// Gets the UI culture context item.
        /// </summary>
        /// <value>The UI culture context item.</value>
        public ContextItem UICultureContextItem
        {
            get
            {
                if (string.IsNullOrEmpty(KeyofUICulture))
                {
                    SetContext(KeyofUICulture, CultureInfo.CurrentUICulture.Name);
                }
                return this[KeyofUICulture];
            }
        }
        #endregion
    }
}